﻿Imports System.Runtime.Serialization

<CLSCompliant(True), Serializable(), DataContract>
Public Class CFolderHash : Inherits List(Of CFileHash)

    Shared Sub New()
        Try
            CProto.Prepare(Of CFolderHash)()
        Catch
        End Try
    End Sub

#Region "Hashing"
    Private m_hash As Guid = Guid.Empty
    Public ReadOnly Property Hash As Guid
        Get
            If m_hash = Guid.Empty AndAlso Me.Count > 0 Then
                Me.Sort()

                'Include filename in hash (e.g. may be repeated/moved files)
                Dim sb As New Text.StringBuilder
                For Each i As CFileHash In Me
                    sb.Append(i.Name.Replace("\", "/")).Append(i.MD5)
                Next
                m_hash = CBinary.MD5_(sb.ToString())
            End If
            Return m_hash
        End Get
    End Property
    Public ReadOnly Property Base64 As String
        Get
            Return CBinary.ToBase64(Hash)
        End Get
    End Property
    Public ReadOnly Property Base64Trunc As String
        Get
            Return CBinary.ToBase64(Hash, 10)
        End Get
    End Property
#End Region

#Region "Ageing"
    Private m_created As DateTime = DateTime.Now
    Public ReadOnly Property Created As DateTime
        Get
            Return m_created
        End Get
    End Property
    Public ReadOnly Property Age As TimeSpan
        Get
            Return DateTime.Now.Subtract(Created)
        End Get
    End Property
#End Region

#Region "Export/Import"
    Public Function Export() As Dictionary(Of String, Guid)
        Dim d As New Dictionary(Of String, Guid)(Me.Count)
        For Each i As CFileHash In Me
            d.Add(i.Name, i.MD5)
        Next
        Return d
    End Function
    Private Sub Import(d As Dictionary(Of String, Guid))
        Me.Clear()
        For Each i As String In d.Keys
            Me.Add(New CFileHash(i, d(i)))
        Next
        m_hash = Guid.Empty
    End Sub
#End Region

#Region "Constructor"

    Public Const IGNORE_EXTENSIONS_AS_STRING As String = ".txt,.config,.vssscc,.scc,.pubxml,.publishproj"
    Public Shared ReadOnly DEFAULT_EXCEPT_EXTENSIONS As String() = {".txt", ".config", ".scc", ".vssscc", ".pubxml", ".publishproj"}
    Protected Sub New()
        MyBase.New
    End Sub
    Public Sub New(ByVal folderPath As String, Optional ByVal exceptExtensions As String() = Nothing, Optional ByVal recursive As Boolean = False)
        Me.New(GetFiles(folderPath, recursive), exceptExtensions, folderPath)
    End Sub
    Public Sub New(ByVal files As String(), Optional ByVal exceptExtensions As String() = Nothing, Optional ByVal baseFolder As String = Nothing)
        MyBase.New(files.Length)

        If IsNothing(exceptExtensions) Then exceptExtensions = DEFAULT_EXCEPT_EXTENSIONS

        If Not IsNothing(baseFolder) Then
            If Not baseFolder.EndsWith("/") AndAlso Not baseFolder.EndsWith("\") Then baseFolder &= "\"
        End If

        'Hash the files
        For Each i As String In files
            i = i.ToLower

            If i.Contains("vshost.") Then Continue For

            If EndsWith(i, exceptExtensions) Then Continue For

            Dim f As New CFileHash(i)
            Add(f)

            'Add any parent subfolders to filenames 
            If Not IsNothing(baseFolder) Then
                Dim thisFolder As String = String.Concat(IO.Path.GetDirectoryName(i), "\")
                If thisFolder.Length > baseFolder.Length Then
                    Dim subfolder As String = thisFolder.Substring(baseFolder.Length)
                    f.Name = String.Concat(subfolder, f.Name)
                End If
            End If
        Next
    End Sub
    Public Sub New(importFrom As Dictionary(Of String, Guid))
        MyBase.New(importFrom.Count)
        Me.Import(importFrom)
    End Sub
    Public Sub New(importFrom As List(Of CFileHash))
        MyBase.New(importFrom)
    End Sub



    Private Shared Function EndsWith(s As String, ss As String()) As Boolean
        For Each i As String In ss
            If s.EndsWith(i) Then Return True
        Next
        Return False
    End Function
    Private Shared Function GetFiles(ByVal folderPath As String, ByVal recursive As Boolean) As String()
        If Not IO.Directory.Exists(folderPath) Then Return New String() {} 'Suppress exceptions
        If Not recursive Then Return IO.Directory.GetFiles(folderPath)

        Dim list As New List(Of String)
        list.AddRange(GetFiles(folderPath, False))
        For Each i As String In IO.Directory.GetDirectories(folderPath)
            list.AddRange(GetFiles(i, True))
        Next
        Return list.ToArray
    End Function
#End Region

#Region "Index On Name"
	Public Function Has(key As String) As Boolean
		Return Dict.ContainsKey(key.ToLower)
	End Function
	Public Function Has(key As Guid) As Boolean
		Return DictHash.ContainsKey(key)
	End Function


	Public Function GetFile(key As String) As CFileHash
		Dim c As CFileHash = Nothing
		Dict.TryGetValue(key.ToLower, c)
		Return c
	End Function
	Public Function GetFiles(key As Guid) As List(Of String)
		Dim c As List(Of String) = Nothing
		DictHash.TryGetValue(key, c)
		Return c
	End Function
	Public Function GetFiles(key As String) As List(Of Guid)
		Dim list As New List(Of Guid)(1)
		For Each i As CFileHash In Me
			If i.Name.ToLower = key.ToLower Then list.Add(i.MD5)
		Next
		Return list
	End Function


	<NonSerialized()> Private m_dict As Dictionary(Of String, CFileHash)
	Public ReadOnly Property Dict As Dictionary(Of String, CFileHash)
		Get
			If IsNothing(m_dict) OrElse m_dict.Count <> Me.Count Then
				m_dict = New Dictionary(Of String, CFileHash)(Me.Count)
				For Each i As CFileHash In Me
					m_dict(i.Name.ToLower) = i
				Next
			End If
			Return m_dict
		End Get
	End Property
	<NonSerialized()> Private m_dictHash As Dictionary(Of Guid, List(Of String))
	Public ReadOnly Property DictHash As Dictionary(Of Guid, List(Of String))
		Get
			If IsNothing(m_dictHash) Then
				Dim list As List(Of String) = Nothing
				m_dictHash = New Dictionary(Of Guid, List(Of String))(Me.Count)
				For Each i As CFileHash In Me
					If Not m_dictHash.TryGetValue(i.MD5, list) Then
						list = New List(Of String)(1)
						m_dictHash.Add(i.MD5, list)
					End If
					list.Add(i.Name)
				Next
			End If
			Return m_dictHash
		End Get
	End Property


	Private m_names As List(Of String)
	Public ReadOnly Property Names As List(Of String)
		Get
			If IsNothing(m_names) OrElse m_names.Count <> Count Then
				Dim list As New List(Of String)(Dict.Keys)
				list.Sort()
				m_names = list
			End If
			Return m_names
		End Get
	End Property

	Private m_hashes As List(Of Guid)
	Public ReadOnly Property Hashes As List(Of Guid)
		Get
			If IsNothing(m_hashes) Then
				Dim list As New List(Of Guid)(Me.Count)
				For Each i As CFileHash In Me
					list.Add(i.MD5)
				Next
				m_hashes = list
			End If

			Return m_hashes
		End Get
	End Property

	Public Function DiffOnHash(other As CFolderHash) As CDiff_Guid
		Return CDiffLogic.Diff(Hashes, other.Hashes)
	End Function
	Public Function DiffOnName(other As CFolderHash) As CDiff_String
		Return CDiffLogic.Diff(Names, other.Names)
	End Function
#End Region

#Region "Diff Logic (filesystem)"
	Public Shared Function FromPath(folderPath As String, Optional exceptExtensions As String() = Nothing, Optional recursive As Boolean = True) As CFilesList
        Dim temp As New CFolderHash(folderPath, exceptExtensions, recursive)
        Return temp.ResolveDifferences(New CFolderHash, folderPath)
    End Function

    Public Function ResolveDifferences(old As CFolderHash, folderPath As String) As CFilesList
        Dim diff As New CFilesList
        If Not folderPath.EndsWith("/") AndAlso Not folderPath.EndsWith("\") Then folderPath &= "\"

        'Inserts, Updates
        For Each i As CFileHash In Me
            Dim j As CFileHash = Nothing
            If Not old.Dict.TryGetValue(i.Name, j) OrElse j.MD5 <> i.MD5 Then
                diff.Add(AddOrUpdate(i.Name, folderPath))
            End If
        Next
        For Each i As CFileHash In old
            If Not Me.Dict.ContainsKey(i.Name) Then diff.Add(Delete(i.Name))
        Next

        Return diff
    End Function
    Private Function AddOrUpdate(name As String, folderPath As String) As CFileNameAndContent
        Return New CFileNameAndContent(folderPath, name)
    End Function
    Private Function Delete(name As String) As CFileNameAndContent
        Dim fnc As New CFileNameAndContent
        fnc.Name = name
        fnc.Content = Nothing
        Return fnc
    End Function
#End Region

#Region "Use Diff results to apply an upgrade"
    Public Shared Sub ApplyChanges(differences As List(Of CFileNameAndContent), folderPath As String)
        If Not folderPath.EndsWith("/") AndAlso Not folderPath.EndsWith("\") Then folderPath &= "\"

        For Each i As CFileNameAndContent In differences
            Dim filePath As String = folderPath & i.Name
            Try
                If IO.File.Exists(filePath) Then IO.File.Delete(filePath)
            Catch

                If IO.File.Exists(filePath) Then
                    IO.File.SetAttributes(filePath, IO.FileAttributes.Normal)
                    IO.File.Delete(filePath)
                End If
            End Try
            If IO.File.Exists(filePath) Then IO.File.Delete(filePath)

            Dim subFolder As String = IO.Path.GetDirectoryName(filePath)
            If Not IO.Directory.Exists(subFolder) Then IO.Directory.CreateDirectory(subFolder)
            If Not IsNothing(i.Content) Then IO.File.WriteAllBytes(filePath, i.Content)
        Next
    End Sub

    'Special case: self-upgrading an exe
    Public Function ApplyDeletesOnly(differences As List(Of CFileNameAndContent), folderPath As String) As Integer
        Return ApplyDeletesOnly(differences, folderPath, False)
    End Function
    Public Function ApplyDeletesOnly(differences As List(Of CFileNameAndContent), folderPath As String, deletesAsAnEmptyFileWithExtension As Boolean, Optional extensionToMarkForDelete As String = ".delete") As Integer
        If Not folderPath.EndsWith("/") AndAlso Not folderPath.EndsWith("\") Then folderPath &= "\"

        Dim count As Integer = 0
        For Each i As CFileNameAndContent In differences
            If Not IsNothing(i.Content) Then Continue For
            count += 1

            Dim filePath As String = folderPath & i.Name

            If deletesAsAnEmptyFileWithExtension Then
                'Defer the delete to upgrade.exe (so current exe can exit first)
                IO.File.WriteAllText(filePath & extensionToMarkForDelete, String.Empty)
            Else
                Try
                    'Immediate delete (default behaviour)
                    If IO.File.Exists(filePath) Then IO.File.Delete(filePath)
                Catch
                    'Deferred delete
                    i.Name &= ".delete"
                    i.Content = New Byte() {}
                End Try
            End If
        Next
        Return count
    End Function
    Public Function ApplyAddOrUpdatesOnly(differences As List(Of CFileNameAndContent), folderPath As String) As Integer
        If Not folderPath.EndsWith("/") AndAlso Not folderPath.EndsWith("\") Then folderPath &= "\"

        Dim count As Integer = 0
        For Each i As CFileNameAndContent In differences
            If IsNothing(i.Content) Then Continue For 'delete
            count += 1

            'Add or Upate
            Dim filePath As String = folderPath & i.Name
            If IO.File.Exists(filePath) Then IO.File.Delete(filePath)
            Dim actualFolder As String = IO.Path.GetDirectoryName(filePath)
            If Not IO.Directory.Exists(actualFolder) Then IO.Directory.CreateDirectory(actualFolder)
            If Not IsNothing(i.Content) Then IO.File.WriteAllBytes(filePath, i.Content)
        Next
        Return count
    End Function
#End Region


#Region "Checks"
    Public Function DetectNew(list As CFolderHash) As CFolderHash
        Dim temp As New CFolderHash
        For Each i As CFileHash In list
            If Not Me.Has(i.Name) Then temp.Add(i)
        Next
        Return temp
    End Function
    Public Function DetectMissing(list As CFolderHash) As CFolderHash
        Return list.DetectNew(Me)
    End Function
    Public Function DetectCommon(list As CFolderHash) As CFolderHash
        Dim temp As New CFolderHash
        For Each i As CFileHash In list
            If Me.Has(i.Name) Then temp.Add(i)
        Next
        Return temp
    End Function
    Public Function DetectDifferent(list As CFolderHash) As CFolderHash
        Dim temp As New CFolderHash
        For Each i As CFileHash In list
            Dim j As CFileHash = Me.GetFile(i.Name)
            If IsNothing(j) Then Continue For
            If i.MD5 <> j.MD5 Then temp.Add(i)
        Next
        Return temp
    End Function
#End Region

End Class
